<?php
/**
 * Created by PhpStorm.
 * User: qch
 * Date: 2015/8/3
 * Time: 18:05
 */

namespace J\UI\Engine;


use J\Exception;
use J\Message;
use J\Engine;
use J\UI\Engine\Thinkphp\TagLib;

/**
 * ThinkPHP内置模板引擎类
 * 支持XML标签和普通标签的模板解析
 * 编译型模板引擎 支持动态缓存
 *
 */
class Thinkphp implements Engine
{
    protected $begin = '\{';
    protected $end = '\}';
    protected $tagBegin = '<';
    protected $tagEnd = '>';
    protected $orgBegin = '{';
    protected $orgEnd = '}';
    protected $builtinTag = ['Cx'];

    protected $template;
    protected $dir;



    // 模板页面中引入的标签库列表
    protected $tagLib = array();
    private   $literal = array();
    private   $block = array();

    /**
     * @inheritdoc
     */
    public function fetch($template)
    {

        $this->template = basename($template);
        $this->dir      = dirname($template);
        return $this->compiler();
    }

    /**
     * @param $content
     * @return mixed|string
     * @throws Exception
     */
    public function content_compiler($content)
    {
        # 内容为空不解析
        if (empty($content)) return '';

        # 解析继承
        $content = $this->parseExtend($content);

        # 检查PHP语法
        $content = $this->parsePhp($content);

        //模板解析
        $content = $this->parse($content);

        // 优化生成的php代码
        $content = str_replace('?><?php', '', $content);

        app()->debug(Message::COMPILE_COMPLETE);

        return $content;
    }

    /**
     * 编译模板文件内容
     *
     * @return string
     * @throws Exception
     */
    public function compiler()
    {
        $content = static::getTemplateContents($this->dir . DS . $this->template);

        app()->debug(Message::COMPILE_FILE_BEGIN, ['file' => $this->template]);

        return $this->content_compiler($content);


    }

    /**
     * 编译模板继承信息
     * @param string $content
     * @param string $basedir
     * @return string
     * @throws Exception
     */
    protected function parseExtend($content, $basedir = null)
    {
        // 读取模板中的继承标签
        $find = preg_match('/' . $this->tagBegin . 'extend\s(.+?)\s*?\/' . $this->tagEnd . '/is', $content, $matches);
        $dir = $basedir;
        if ($find) {
            //替换extend标签
            $array = $this->parseXmlAttrs($matches[1]);
            $dir   = $this->getFullFileDir(array_isset($array,'dir',$basedir));
            $file  = $dir . DS . $array['name'];

            $parent = $this->getTemplateContents($file);
            $parent = $this->parseExtend($parent,dirname($file));
            $content = str_replace($matches[0], $parent, $content);
        }
        # 检查include语法
        $content = $this->parseInclude($content,$dir);
        return $content;
    }

    /**
     * 编译模板包含信息
     * @param string $content
     * @param string $basedir
     *
     * @return string
     *
     * @throws Exception
     */
    protected function parseInclude($content,$basedir = null)
    {
        // 读取模板中的include标签
        $find = preg_match_all('/' . $this->tagBegin . 'include\s(.+?)\s*?\/' . $this->tagEnd . '/is', $content, $matches);
        if ($find) {
            for ($i = 0; $i < $find; $i++) {
                $include = $matches[1][$i];
                $array = $this->parseXmlAttrs($include);
                $dir  = $this->getFullFileDir(array_isset($array,'dir',$basedir));
                $file = $dir . DIRECTORY_SEPARATOR . $array['name'];

                // 分析模板文件名并读取内容
                $parseStr = $this->getTemplateContents($file);
                unset($array['file']);

                // TODO 模板的多态化😊
                foreach ($array as $key => $val) {
                    $parseStr = str_replace('[' . $key . ']', $val, $parseStr);
                }

                // 再次对包含文件进行模板分析
                $content = str_replace($matches[0][$i], $this->parseInclude($parseStr, dirname($file)), $content);
            }
        }
        return $content;
    }

    /**
     * 预处理php标签
     * @param string $content
     * @return string
     */
    protected function parsePhp($content)
    {
        if (ini_get('short_open_tag')) {
            // 开启短标签的情况要将<?标签用echo方式输出 否则无法正常输出xml标识
            $content = preg_replace('/(<\?(?!php|=|$))/i', '<?php echo \'\\1\'; ?>' . "\n", $content);
        }

        return $content;
    }

    /**
     * 模板解析正式入口
     * 支持普通标签和TagLib解析 支持自定义标签库
     *
     * @param string $content 要解析的模板内容
     *
     * @return string
     * @throws Exception
     */
    public function parse($content)
    {
        # 内容为空不解析
        if (empty($content)) return '';

        # 首先替换literal标签内容
        $content = preg_replace_callback('/' . $this->tagBegin . 'literal' . $this->tagEnd . '(.*?)' . $this->tagBegin . '\/literal' . $this->tagEnd . '/is', array($this, 'parseLiteral'), $content);

        # 转换所有的Block标签
        $content = $this->prepareBlock($content);
        $content = $this->replaceBlock($content);

        // TODO 自动载入自定义标签库

        // 内置标签库 无需使用taglib标签导入就可以使用 并且不需使用标签库XML前缀
        foreach ($this->builtinTag as $tag) {
            $this->parseTagLib($tag, $content);
        }

        //解析普通模板标签 {tagName}
        $content = preg_replace_callback('/(' . $this->begin . ')([^\d\s' . $this->begin . $this->end . '].+?)(' . $this->end . ')/is', array($this, 'parseContent'), $content);;

        // 还原被替换的Literal标签
        $content = preg_replace_callback('/<!--###literal(\d+)###-->/is', array($this, 'restoreLiteral'), $content);

        return $content;
    }

    /**
     * 模板标签解析
     * 格式： {TagName:args [|content] }
     * @access public
     * @param string $tagStr 标签内容
     * @param bool $asPHP 使用PHP标签
     * @return string
     */
    public function parseExpression($tagStr, $asPHP = true)
    {
        $begin = $asPHP ? '<?php ' : '';
        $end = $asPHP ? '; ?>' : '';
        $echo = $asPHP ? 'echo ' : '';
        if (is_array($tagStr)) $tagStr = $tagStr[2];
        $tagStr = stripslashes($tagStr);
        //}
        //还原非模板标签
        if (preg_match('/^[\s|\d]/is', $tagStr))
            //过滤空格和数字打头的标签
            return false;

        $flag = substr($tagStr, 0, 1);
        $flag2 = substr($tagStr, 1, 1);
        $name = substr($tagStr, 1);
        if ('$' == $flag && '.' != $flag2 && '(' != $flag2) { //解析模板变量 格式 {$varName}
            return $begin . $echo . $this->parseVar($name) . $end;
        } elseif ('-' == $flag || '+' == $flag) { // 输出计算
            return $begin . $echo . $flag . $name . $end;
        } elseif (':' == $flag) { // 输出某个函数的结果
            return $begin . $echo . $name . $end;
        } elseif ('~' == $flag) { // 执行某个函数
            return $begin . $name . $end;
        } elseif (substr($tagStr, 0, 2) == '//' || (substr($tagStr, 0, 2) == '/*' && substr(rtrim($tagStr), -2) == '*/')) {
            //注释标签
            return '';
        } else {
            return false;
        }
    }

    /**
     * 解析普通模板标签
     * @param string $content
     * @return string
     */
    protected function parseContent($content)
    {
        $expression = $this->parseExpression($content, true);
        if ($expression === false) {
            if (is_array($content)) $content = $content[2];
            return $this->orgBegin . $content . $this->orgEnd;
        } else {
            return $expression;
        }
    }

    /**
     * 模板变量解析,支持使用函数
     * 格式： {$var|function1|function2=arg1,arg2}
     * @access public
     * @param string $varStr 变量数据
     * @return string  php代码
     */
    public function parseVar($varStr)
    {
        $varStr = trim($varStr);
        static $_varParseList = array();
        //如果已经解析过该变量字串，则直接返回变量值
        if (isset($_varParseList[$varStr])) return $_varParseList[$varStr];
        $name = '';
        //$varExists  =   true;
        if (!empty($varStr)) {
            $varArray = explode('|', $varStr);
            //取得变量名称
            $var = array_shift($varArray);
            if ('Think.' == substr($var, 0, 6)) {
                // 所有以Think.打头的以特殊变量对待 无需模板赋值就可以输出
                $name = $this->parseThinkVar($var);
            } elseif (false !== strpos($var, '.')) {
                //支持 {$var.property}
                $vars = explode('.', $var);
                $var = array_shift($vars);
                $name = 'is_array($' . $var . ')?';
                $name .= '$' . $var . '[\'' . implode('\'][\'', $vars) . '\']';
                $name .= ':';
                $name .= '$' . $var . '->' . implode('->', $vars);
                $name .= '';
            } elseif (false !== strpos($var, '[')) {
                $name = "$" . $var;
            } elseif (false !== strpos($var, ':') && false === strpos($var, '(') && false === strpos($var, '::') && false === strpos($var, '?')) {
                //支持 {$var:property} 方式输出对象的属性
                $var = str_replace(':', '->', $var);
                $name = "$" . $var;
            } else {
                $name = "$" . $var;
            }
            //对变量使用函数
            if (count($varArray) > 0)
                $name = $this->parseVarFunction($name, $varArray);
        }
        $_varParseList[$varStr] = $name;
        app()->debug(Message::COMPILE_VAR, ['name' => $name]);
        return $name;
    }

    /**
     * 特殊模板变量解析
     * 格式 以 $Think. 打头的变量属于特殊模板变量
     * @access public
     * @param string $varStr 变量字符串
     * @return string
     */
    public function parseThinkVar($varStr)
    {
        $vars = explode('.', $varStr);;
        # drop Think.
        array_shift($vars);

        $option = strtoupper(trim(array_shift($vars)));
        $parseStr = '';

        switch ($option) {
            case 'CONST':
                $parseStr = strtoupper(array_shift($vars));
                break;
            case 'LANG':
                $parseStr = '("' . array_shift($vars) . '")';
                break;
            case 'CONFIG':
                $parseStr = 'C(' . implode('.', $vars) . ')';
                break;
            case 'NOW':
                $parseStr = "date('Y-m-d g:i a',time())";
                break;
            case 'TEMPLATE':
                $parseStr = "'" . $this->template . "'";
                break;
            case 'BEGIN':
                $parseStr = "'" . $this->orgBegin . "'";
                break;
            case 'END':
                $parseStr = "'" . $this->orgEnd . "'";
                break;
            default:
                if (defined($option)) $parseStr = $option;
        }

        return $parseStr;
    }

    /**
     * 对模板变量使用函数
     * 格式 {$var|function1|function2=arg1,arg2}
     * @access public
     * @param string $name 变量名
     * @param array $varArray 函数列表
     * @return string
     */
    public function parseVarFunction($name, $varArray)
    {
        //对变量使用函数
        $length = count($varArray);
        //取得模板禁止使用函数列表
        $template_deny_functions = app()->get('TEMPLATE_DENY_FUNCTION_LIST');
        for ($i = 0; $i < $length; $i++) {
            $args = explode('=', $varArray[$i], 2);
            //模板函数过滤
            $fun = strtolower(trim($args[0]));
            switch ($fun) {
                case 'default':  // 特殊模板函数
                    $name = '(isset(' . $name . ') && (' . $name . ' !== ""))?(' . $name . '):' . $args[1];
                    break;
                default:  // 通用模板函数
                    if (!in_array($fun, $template_deny_functions)) {
                        if (isset($args[1])) {
                            if (strstr($args[1], '###')) {
                                $args[1] = str_replace('###', $name, $args[1]);
                                $name = "$fun($args[1])";
                            } else {
                                $name = "$fun($name,$args[1])";
                            }
                        } else if (!empty($args[0])) {
                            $name = "$fun($name)";
                        }
                    }
            }
        }
        return $name;
    }


    protected function prepareBlock($content)
    {
        // 记录页面中的block标签
        return preg_replace_callback('/' . $this->tagBegin . 'block\sname=[\'"](.+?)[\'"]\s*?' . $this->tagEnd . '(.*?)' . $this->tagBegin . '\/block' . $this->tagEnd . '/is', array($this, 'parseBlock'), $content);
    }

    /**
     * 记录当前页面中的block标签
     * @access private
     * @param string $name block名称
     * @param string $content 模板内容
     * @return string
     */
    private function parseBlock($name, $content = '')
    {
        if (is_array($name)) {
            $content = $name[2];
            $name = $name[1];
        }
        $this->block[$name] = $content;
        return '';
    }

    /**
     * 替换继承模板中的block标签
     * @access private
     * @param string $content 模板内容
     * @return string
     */
    private function replaceBlock($content)
    {
        $reg = '/(' . $this->tagBegin . 'placeholder\sname=[\'"](.+?)[\'"]\s*?' . $this->tagEnd . ')(.*?)' . $this->tagBegin . '\/placeholder' . $this->tagEnd . '/is';
        preg_match_all($reg, $content, $matches, PREG_SET_ORDER);
        foreach ($matches as $val) {
            $name = $val[2];
            $original = $val[3];
            $replace = isset($this->block[$name]) ? $this->block[$name] : $original;
            $replace = $this->replaceBlock($replace);
            $content = str_replace($val[0], $replace, $content);
        }
        return $content;
    }


    /**
     * TagLib库解析
     *
     * @param string $tagLib 要解析的标签库
     * @param string $content 要解析的模板内容
     *
     * @throws Exception
     */
    public function parseTagLib($tagLib, &$content)
    {
        $begin = $this->tagBegin;
        $end = $this->tagEnd;

        $tLib = $this->getLib($tagLib);

        if (!$tLib) {
            return;
        }

        foreach ($tLib->getTags() as $name => $val) {
            $closeTag = isset($val['close']) ? $val['close'] : true;
            $n1 = (!isset($val['attr']) or empty($val['attr'])) ? '(\s*?)' : '\s([^' . $end . ']*)';

            if (!$closeTag) {
                $patterns = '/' . $begin . $name . $n1 . '\/(\s*?)' . $end . '/is';
                $that = $this;
                $content = preg_replace_callback($patterns, function ($matches) use ($tLib, $name, $that) {
                    return $tLib->parseXmlTag($name, $that->parseXmlAttrs($matches[1]), $matches[2]);
                }, $content);
            } else {

                $patterns = '/' . $begin . $name . $n1 . $end . '((.*?)' . $begin . '\/' . $name . '(\s*?)' . $end . ')/is';
                $content = $this->parseNest($tLib, $name, $patterns, $content);
            }
        }
    }

    /**
     * @param string $tagLib
     * @return bool|TagLib
     */
    protected function getLib($tagLib)
    {
        return TagLib::getTagLib($tagLib, $this);
    }

    /**
     * @param TagLib $tLib
     * @param string $name
     * @param string $patterns
     * @param string $content
     * @return mixed
     * @throws Exception
     */
    protected function parseNest($tLib, $name, $patterns, $content)
    {
        while (preg_match($patterns, $content, $matchesUp)) {
            if (preg_match($patterns, $matchesUp[2], $matchesDown)) {
                //var_dump($matchesUp);
                //echo '<br/> Nest!!!!<br/>';
                //var_dump($matchesDown);
                $replace = $this->parseNest($tLib, $name, $patterns, $matchesUp[2]);
                //echo "\n",$replace,"\n";
                $content = str_replace($matchesUp[2], $replace, $content);
                $content = $this->parseNest($tLib, $name, $patterns, $content);
            } else {
                //var_dump($matchesUp);
                $content = str_replace($matchesUp[0], $tLib->parseXmlTag($name, $this->parseXmlAttrs($matchesUp[1]), $matchesUp[3]), $content);
            }
        }
        return $content;
    }



    /**
     * 替换页面中的literal标签
     * @access private
     * @param string $content 模板内容
     * @return string|false
     */
    private function parseLiteral($content)
    {
        if (is_array($content)) $content = $content[1];
        if (trim($content) == '') return '';
        $i = count($this->literal);
        $parseStr = "<!--###literal{$i}###-->";
        $this->literal[$i] = $content;
        return $parseStr;
    }

    /**
     * 还原被替换的literal标签
     * @access private
     * @param string $tag literal标签序号
     * @return string|false
     */
    private function restoreLiteral($tag)
    {
        if (is_array($tag)) $tag = $tag[1];
        // 还原literal标签
        $parseStr = $this->literal[$tag];
        // 销毁literal记录
        unset($this->literal[$tag]);
        return $parseStr;
    }


    ///////////////////////////内部工具//////////////////////////////
    /**
     * 分析加载的模板文件并读取内容
     * @param string $template 模板文件名
     *
     * @return string
     */
    protected static function getTemplateContents($template)
    {
        if (is_file($template))
            return file_get_contents($template);
        app()->error('NOT_FIND_TEMPLATE_FILE {file}', ['file' => $template]);
        return '';
    }

    protected function getFullFileDir($dir = null)
    {
        if($dir === null ){
            $dir = $this->dir;
        }elseif($dir === '@'){
            $dir = DEFAULT_VIR;
        }
        return $dir;
    }

    /**
     * 分析XML属性
     * @param string $attrs XML属性字符串
     * @return array
     * @throws Exception
     */
    private function parseXmlAttrs($attrs)
    {
        $xml = '<tpl><tag ' . $attrs . ' /></tpl>';
        $xml = simplexml_load_string($xml);
        if (!$xml)
            throw new Exception('XML_TAG_ERROR:{attrs}', ['attrs' => $attrs]);
        /** @var \stdClass $xml */
        $xml = (array)($xml->tag->attributes());

        if (!isset($xml['@attributes'])) return [];

        $array = array_change_key_case($xml['@attributes']);
        return $array;
    }


    //////////////////////////////get/set///////////////////////////////
    /**
     * @param string $begin
     */
    public function setBegin($begin)
    {
        $this->orgBegin = $begin;
        $this->begin = $this->stripPreg($begin);
    }

    /**
     * @param string $end
     */
    public function setEnd($end)
    {
        $this->orgEnd = $end;
        $this->end = $this->stripPreg($end);
    }

    /**
     * @param string $tagBegin
     */
    public function setTagBegin($tagBegin)
    {
        $this->tagBegin = $this->stripPreg($tagBegin);
    }

    /**
     * @param string $tagEnd
     */
    public function setTagEnd($tagEnd)
    {
        $this->tagEnd = $this->stripPreg($tagEnd);
    }

    /**
     * @param array $presetTag
     */
    public function setPresetTag($presetTag)
    {
        $this->tagLib = $this->append($this->tagLib, $presetTag);

    }

    /**
     * @param array $builtinTag
     */
    public function setBuiltinTag($builtinTag)
    {
        $this->builtinTag = $this->append($this->builtinTag, $builtinTag);
    }

    private function stripPreg($str)
    {
        return str_replace(
            array('{', '}', '(', ')', '|', '[', ']', '-', '+', '*', '.', '^', '?'),
            array('\{', '\}', '\(', '\)', '\|', '\[', '\]', '\-', '\+', '\*', '\.', '\^', '\?'),
            $str);
    }

    private function append($key, $value)
    {
        if (!is_array($value)) {
            $value = explode(',', $value);
        }
        return array_merge($key, $value);
    }



}